package indigo.mybatis.dao;

import static java.util.Objects.requireNonNull;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;

public class Update implements Serializable {
	private static final long serialVersionUID = 5832066841993872240L;

	private final String property;
	private final Operation operation;
	private final List<Object> parameters;

	public Update(String property, Operation operation, Object... parameters) {
		this(property, operation, Arrays.asList(parameters));
	}

	public Update(String property, Operation operation, Collection<?> parameters) {
		this.property = requireNonNull(property, "property cannot be null");
		this.operation = requireNonNull(operation, "operation cannot be null");
		this.parameters = checkParameters(requireNonNull(parameters, "parameters cannot be null"), operation);
	}

	public String getProperty() {
		return property;
	}

	public Operation getOperation() {
		return operation;
	}

	public List<?> getParameters() {
		return parameters;
	}

	@Override
	public String toString() {
		return String.format("Update {property=%s, operation=%s, parameters=%s}",
				property, operation.name(), parameters);
	}

	public static enum Operation {
		ASSIGN(1, 1), ADD(1, 1), SUBTRACT(1, 1);

		private final int maxParameterCount;
		private final int minParameterCount;

		private Operation(int maxParameterCount, int minParameterCount) {
			this.maxParameterCount = maxParameterCount;
			this.minParameterCount = minParameterCount;
		}

		public int getMaxParameterCount() {
			return maxParameterCount;
		}

		public int getMinParameterCount() {
			return minParameterCount;
		}
	}

	private static List<Object> checkParameters(Collection<?> parameters, Operation operation) {
		int parameterCount = parameters.size();
		if (parameterCount < operation.getMinParameterCount()) {
			throw new IllegalStateException(String.format("Too few parameters for operation '%s': %d",
					operation.name(), parameterCount));
		}
		if (parameterCount > operation.getMaxParameterCount()) {
			throw new IllegalStateException(String.format("Too many parameters for operation '%s': %d",
					operation.name(), parameterCount));
		}
		return ImmutableList.copyOf(parameters);
	}

	public static Update assign(String property, Object parameter) {
		return new Update(property, Operation.ASSIGN, parameter);
	}

	public static Update add(String property, Number parameter) {
		return new Update(property, Operation.ADD, parameter);
	}

	public static Update subtract(String property, Number parameter) {
		return new Update(property, Operation.SUBTRACT, parameter);
	}

	public static Set<Update> assignAll(Object obj) {
		return assignAll(obj, null);
	}

	public static Set<Update> assignAll(Object obj, Predicate<Update> filter) {
		Set<Update> updates = Sets.newHashSet();
		if (obj instanceof Map) {
			for (Map.Entry<?, ?> entry : ((Map<?, ?>) obj).entrySet()) {
				updates.add(assign((String) entry.getKey(), entry.getValue()));
			}
		} else {
			MetaObject metaBean = SystemMetaObject.forObject(obj);
			for (String propName : metaBean.getGetterNames()) {
				updates.add(assign(propName, metaBean.getValue(propName)));
			}
		}
		if (filter != null) {
			updates = updates.stream().filter(filter).collect(Collectors.toSet());
		}
		return Collections.unmodifiableSet(updates);
	}
}
